Перейти к основному содержимому

9. Безопасность GraphQL, авторизация, IAM Keycloak

В данном разделе мы рассмотрим основы безопасности GraphQL, авторизации и IAM Keycloak. Также будут даны рекомендации по интеграции механизмов безопасности GraphQL (проверка и фильтрация) в React-приложение медицинской клиники.

Основы авторизации

Современные веб-приложения требуют надежных механизмов контроля доступа для защиты данных и обеспечения персонализированного опыта пользователей. В контексте нашего React-приложения медицинской клиники особенно важно разграничить доступ между различными ролями пользователей.

Ключевые концепции

  • Аутентификация — процесс проверки подлинности пользователя (кто это?). Аутентификация проверяет, что пользователь действительно является тем, за кого он себя выдает.

  • Авторизация — процесс определения прав доступа пользователя к ресурсам (что может делать?). Авторизация проверяет, что пользователь имеет право выполнять определенные действия над определенными ресурсами.

При этом оба процесса взаимосвязаны и не могут быть разделены. Аутентификация проверяет, кто пользователь, а авторизация проверяет, что пользователь может делать.

Современный подход к авторизации

Для обеспечения безопасности и удобства в нашем приложении мы будем использовать стандартные инструменты:

  • OpenID Connect для аутентификации пользователей
  • JWT токены для передачи информации о правах доступа и аутентификации
  • Keycloak как централизованный сервер управления идентификацией и авторизацией

Хоть приложение данного учебного курса и носит демонстрационный характер, но в реальных проектах используются аналогичные инструменты и подходы. Это обеспечивает масштабируемость, безопасность и простоту интеграции компонентов системы. Рассмотрим каждый из этих инструментов более подробно.

OpenID Connect

OpenID Connect (OIDC) — это протокол аутентификации, построенный поверх OAuth 2.0. Он позволяет приложениям безопасно проверять личность пользователя и получать базовую информацию о его профиле.

Основные принципы OpenID Connect:

  • Единый вход (Single Sign-On) — пользователь авторизуется один раз и получает доступ ко всем подключенным приложениям
  • Стандартизированный протокол — использует проверенные механизмы OAuth 2.0 с добавлением слоя идентификации
  • JWT токены — информация о пользователе передается в виде JSON Web Token, который можно проверить без обращения к серверу авторизации
  • Безопасность — все данные передаются по защищенным каналам, токены имеют ограниченное время жизни

Участники процесса:

  • Identity Provider (IdP) — сервер авторизации (например, Keycloak), который проверяет учетные данные пользователя
  • Relying Party (RP) — клиентское приложение (например, React-приложение), которое доверяет IdP
  • End User — пользователь, который проходит аутентификацию

OpenID Connect упрощает интеграцию авторизации в современные веб-приложения, обеспечивая высокий уровень безопасности и удобство для пользователей.

JWT (JSON Web Token)

JWT (JSON Web Token) — это компактный и безопасный способ передачи информации между сторонами в виде JSON-объекта. JWT широко используется для авторизации в веб-приложениях и API.

Структура JWT токена:

JWT состоит из трех частей, разделенных точками: header.payload.signature

  • Header (заголовок) — содержит информацию о типе токена и алгоритме подписи
  • Payload (полезная нагрузка) — содержит данные о пользователе (claims): ID, роли, время истечения, email
  • Signature (подпись) — криптографическая подпись, которая гарантирует целостность токена

Пример JWT токена:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJzdWIiOiJ1c2VyMSIsInJvbGVzIjpbImFkbWluaXN0cmF0b3IiXSwiZW1haWwiOiJ1c2VyMUBtYWlsLnJ1IiwiZXhwIjoxNzM4MjU2NDAwfQ.signature-hash-here

Для анализа JWT токена можно воспользоваться онлайн-сервисом: https://jwt.io/, перейдите по ссылке и вставьте токен в поле JSON Web Token в левой части страницы. В правой части страницы будет отображены все данные токена.

  1. Header:
{
"alg": "RS256",
"typ": "JWT",
"kid": "123"
}

Здесь alg - алгоритм подписи, typ - тип токена, kid - идентификатор ключа.

  1. Payload:
{
"sub": "user1",
"roles": [
"administrator"
],
"email": "user1@mail.ru",
"exp": 1738256400
}

Здесь sub - идентификатор пользователя, roles - роли пользователя, email - email пользователя, exp - время истечения токена, указано в секундах с 1970 года.

Основные преимущества JWT:

  • Самодостаточность — токен содержит всю необходимую информацию, не требуя обращения к базе данных
  • Безопасность — подпись предотвращает подделку токена
  • Компактность — небольшой размер токена позволяет передавать его в HTTP заголовках
  • Стандартизация — открытый стандарт (RFC 7519), поддерживаемый всеми языками программирования

Жизненный цикл JWT:

  1. Создание — сервер авторизации создает и подписывает токен
  2. Передача — клиент получает токен и сохраняет его
  3. Использование — токен отправляется с каждым запросом в заголовке Authorization
  4. Проверка — сервер проверяет подпись и валидность токена
  5. Истечение — токен автоматически становится недействительным через определенное время

JWT обеспечивает надежную и эффективную авторизацию без необходимости хранения состояния на сервере.

IAM Keycloak

Keycloak — это открытый проект, который предоставляет единую платформу для управления доступом к приложениям и сервисам. Он позволяет контролировать доступ пользователей к различным ресурсам, включая веб-приложения, мобильные приложения и API. KeyCloak является свободным ПО с открытым кодом, распространяемым по лицензии Apache License 2.0. Разработан компанией RedHat, Inc. В этом разделе мы рассмотрим основы Keycloak и его практическое применение в проекте React-приложения медицинской клиники.

Основные объекты Keycloak:

  • Realm - область приложения, рабочее пространство, в которой пользователи могут быть авторизованы и получать доступ к различным ресурсам. Realm позволяет настраивать внешний вид страницы логина для каждой отдельной области. Кроме того, в каждом realm возможна регистрация пользователей, авторизация через социальные сети, Single Sign-On/Sign-Off для всех приложений данного realm, выдача JSON Web Token подлинности аккаунтам, двухфакторная аутентификация и интеграция со службами каталогов (LDAP-сервером). Таким образом, realms в Keycloak обеспечивают гибкость и масштабируемость системы безопасности для различных приложений и сервисов.

  • Client - приложение или система, которые использует Keycloak для аутентификации своих пользователей. Клиенты могут быть настроены в Keycloak путем создания нового приложения или выбора существующего. Это позволяет клиентам использовать функции Keycloak, такие как аутентификация, управление пользователями и ролью, а также другие возможности безопасности. Клиент в Keycloak может быть подключен к серверу для предоставления доступа к своим ресурсам пользователям, которые были успешно аутентифицированы Keycloak.

  • User - пользователь системы, который проходит процесс аутентификации и получает доступ к ресурсам. Пользователь обычно имеет уникальный идентификатор и учётную запись, позволяющую ему пройти аутентификацию и получить разрешение на доступ к определённым ресурсам.

  • Role - роль пользователя в системе. Роли определяют права и привилегии, которыми обладает пользователь, и позволяют управлять доступом к ресурсам. Role может быть назначена конкретному пользователю или группе пользователей, она определяет, какие действия они могут выполнять в рамках системы.

Далее будет рассмотрена локальная установка KeyCloak, создание демо-данных для приложения медицинской клиники, а также получение JWKS-ключа KeyCloak для обеспечения проверки подписи JWT-токенов. На основе полученных данных будет рассмотрена безопасность GraphQL и интеграция механизмов безопасности в React-приложение.

Лолальный IAM-сервер KeyCloak

Установка

./bin/kc.sh start-dev --http-port=8180

Запуск KeyCloak на порту 8180 производится с целью избежать конфликта портов с DataSpace CE, который запускается на порту 8080. В реальном проекте KeyCloak может быть запущен на любом свободном порту.

  • Откройте панель управления KeyCloak http://localhost:8180/

  • Система предложит создать учетную запись администратора, укажите логин и парль, например admin 12345

Создание демо-данных KeyCloak

Для демонстрации механизмов безопасности далее будет создан набор демо-данных приложения медицинской клиники.

  • Создайте новый realm с именемclinic

  • Создайте новый client с именем clinic и укажите для него параметр root url : http://localhost:3000/

  • Создайте три client roles:
    • administrator (администратор клиники)

      • может просматривать и изменять все данные в приложении
    • doctor (доктор)

      • просмотр записей кто к нему записался
      • просмотр исследований его пациентов и результатов этих исследований
    • patient (пациент)

      • Просмотр доступности врача (любого)
      • Запись на прием

Важно, что мы создаем именно client roles, а не realm roles. Realm roles используются для управления доступом к ресурсам в рамках всего KeyCloak, а client roles используются для управления доступом к ресурсам в рамках конкретного клиента. Это позволяет более гибко управлять доступом к ресурсам в рамках конкретного приложения, а также обеспечивает безопасность и масштабируемость системы.

  • Создайте трех демо-пользователей:

    • user1 user1@mail.ru
    • user2 user2@mail.ru
    • user3 user3@mail.ru
  • Создайте пароли для пользователей и отключите их временный характер:

  • Присвойте пользователям user1, user2, user3 соответствующие client role:
    • user1 -> administrator
    • user2 -> doctor
    • user3 -> patient

Получение JWKS-ключа KeyCloak:

JWKS (JSON Web Key Set) — это файл, содержащий открытые ключи для проверки подписи JWT токенов. Этот файл критически важен для работы авторизации в связке KeyCloak + DataSpace CE. Он позволяет DataSpace проверять подлинность JWT-токенов, которые будет отправлять React-приложение.

Как это работает:

  1. KeyCloak создает JWT-токен → подписывает его ПРИВАТНЫМ ключом
  2. React приложение получает JWT-токен → отправляет в GraphQL запросах
  3. DataSpace CE получает JWT-токен → проверяет подпись ОТКРЫТЫМ ключом из jwks.json
  4. Если подпись верна → пользователь авторизован, иначе → отказ в доступе

JWKS в DataSpace CE:

В файле context-child.properties (dataspace-ce/files/resources/src-model/) есть важная настройка: dataspace.security.jwks.source=file. По умолчанию данный параметр отключен (закомментирован).

Для настройки использования JWKS-ключа из файла в DataSpace CE выполните следующие действия:

  1. Расскоментируйте параметр dataspace.security.jwks.source=file в файле context-child.properties (dataspace-ce/files/resources/src-model/). Это будет указывать DataSpace CE что при проверке подписи JWT-токенов GraphQL-заппросов необходимо использовать ключ из файла jwks.json.

  2. Перейдите по адресу: http://localhost:8180/realms/clinic/protocol/openid-connect/certs В случае возникновения ошибки проверьте что KeyCloak запущен на порту 8180.

  3. Cкопируйте полученный JWKS-ключ в файл dataspace-ce/files/resources/src-model/jwks.json При необходимости можно произвести визуальноеформатирование ключа для лучше понимания его структуры.

  4. Перезапустите DataSpace CE для применения настроек, выполнив в корневой директории DataSpace CE:

./quickstart.sh

JWKS-ключ содержит массив ключей, каждый из которых имеет свой уникальный идентификатор kid. Таким образом, DataSpace CE может использовать этот ключ для проверки подписи JWT-токенов, отправленных React-приложением. Это позволяет обеспечить безопасность и целостность данных при передаче между компонентами приложения.

Безопасность GraphQL

Иногда приемка созданного приложения у эксперта кибербезопасности можеть быть непростой задачей:

GraphQL предоставляет гибкие возможности для запросов данных, но как при этом обеспечить безопасность, разграничение доступа и фильтрацию данных?

RBAC и ABAC

GraphQL предоставляет гибкие возможности для запросов данных, но при этом требует надежных механизмов контроля доступа. В системах безопасности современных приложений используются два основных подхода: RBAC (Role-Based Access Control) и ABAC (Attribute-Based Access Control).

RBAC (Role-Based Access Control)

RBAC — это модель контроля доступа, основанная на ролях пользователей. В этой модели права доступа определяются ролью, которая назначается пользователю.

Принципы RBAC:

  • Роли — определяют набор разрешений (администратор, врач, пациент)
  • Пользователи — получают роли в соответствии со своими обязанностями
  • Разрешения — привязаны к ролям, а не к конкретным пользователям
  • Иерархия — роли могут наследовать права друг от друга

Пример RBAC в GraphQL-разрешениях:

{
"name": "viewAllPatients",
"checkSelects": [
{
"conditionValue": "'administrator' $in ${jwt:roles}",
"typeName": null
}
]
}

ABAC (Attribute-Based Access Control)

ABAC — это более гибкая модель контроля доступа, основанная на атрибутах субъекта, объекта, действия и окружения.

Компоненты ABAC:

  • Субъект — пользователь и его атрибуты (роль, отдел, специализация)
  • Объект — ресурс и его атрибуты (тип данных, владелец, конфиденциальность)
  • Действие — операция (чтение, запись, удаление)
  • Окружение — контекст (время, IP-адрес, способ аутентификации)

Пример ABAC в GraphQL-разрешениях:

{
"name": "viewPatientRecords",
"checkSelects": [
{
"conditionValue": "'doctor' $in ${jwt:roles} && ${jwt:specialization} == ${object.patientType} && ${environment.hour} >= 8 && ${environment.hour} <= 18",
"typeName": "PatientRecord"
}
],
"pathConditions": [
{
"path": "searchPatientRecord",
"cond": "it.assignedDoctor.entityId == ${jwt:clinicDoctorId} && it.createdDate >= ${environment.workingHoursStart}"
}
]
}

Гибридный подход в DataSpace CE

В реальных медицинских системах часто используется гибридный подход, сочетающий RBAC и ABAC:

1. Базовый уровень (RBAC):

{
"name": "searchDoctorAppointmentForDoctor",
"checkSelects": [
{
"conditionValue": "'doctor' $in ${jwt:roles}",
"typeName": null
}
]
}

2. Дополнительная фильтрация (ABAC):

{
"pathConditions": [
{
"path": "searchDoctorAppointment",
"cond": "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId} && it.beginDate >= ${environment.currentDate}"
}
]
}

Возможные практические примеры для медицинской клиники

Сценарий 1: Врач просматривает свои записи

{
"name": "getDoctorOwnAppointments",
"body": "query getDoctorOwnAppointments { ... }",
"checkSelects": [
{
"conditionValue": "'doctor' $in ${jwt:roles}",
"typeName": null
}
],
"pathConditions": [
{
"path": "searchDoctorAppointment",
"cond": "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId} && it.status != 'CANCELLED'"
}
]
}

Сценарий 2: Администратор с ограничениями по времени

{
"name": "adminViewReports",
"checkSelects": [
{
"conditionValue": "'administrator' $in ${jwt:roles} && ${jwt:workingHours} == true",
"typeName": null
}
]
}

Сценарий 3: Пациент видит только своих врачей

{
"name": "patientViewAssignedDoctors",
"checkSelects": [
{
"conditionValue": "'patient' $in ${jwt:roles}",
"typeName": null
}
],
"pathConditions": [
{
"path": "searchDoctor",
"cond": "it.doctorAppointmentList{cond = it.clinicCustomer.customer.entityId == ${jwt:customerId}}.$exists"
}
]
}

Преимущества каждого подхода

RBAC:

  • ✅ Простота реализации и управления
  • ✅ Легкость понимания для администраторов
  • ✅ Хорошая масштабируемость для типовых сценариев
  • ❌ Ограниченная гибкость для сложных правил

ABAC:

  • ✅ Высокая гибкость и точность контроля
  • ✅ Поддержка сложных бизнес-правил
  • ✅ Возможность динамических разрешений
  • ❌ Сложность в настройке и отладке

В медицинских системах гибридный подход оптимален: базовые роли определяют общие права доступа, а атрибуты позволяют точно настроить доступ к конфиденциальным медицинским данным с учетом специфики лечебного процесса.

Файл graphql-permissions.json

Файл graphql-permissions.json является важным компонентом системы безопасности DataSpace CE, который определяет разрешения для выполнения GraphQL-операций.

Назначение и принципы работы

DataSpace CE использует подход "по умолчанию всё запрещено" — это означает, что при включении параметра dataspace.security.graphql.permissions.source=file в файле context-child.properties (dataspace-ce/files/resources/src-model/) без явного разрешения в файле graphql-permissions.json никакая GraphQL-операция не может быть выполнена. Каждая операция должна:

  1. Быть зарегистрирована в реестре допустимых операций
  2. Иметь уникальное имя — анонимные запросы запрещены
  3. Пройти проверки безопасности — CheckSelects и PathConditions

Структура файла конфигурации

Файл представляет собой JSON-массив объектов, каждый из которых описывает разрешенную операцию:

[
{
"name": "имя_операции",
"body": "полное тело GraphQL запроса",
"allowEmptyChecks": true/false,
"disableJwtVerification": true/false,
"checkSelects": [
{
"conditionValue": "условие проверки",
"typeName": "тип сущности"
}
],
"pathConditions": [
{
"path": "путь к полю",
"cond": "условие фильтрации"
}
]
}
]

Основные параметры конфигурации

  • name — уникальное имя GraphQL-операции (обязательно)
  • body — полный текст GraphQL-запроса для сверки (обязательно)
  • allowEmptyChecks — разрешает пустые проверки безопасности
  • disableJwtVerification — отключает проверку JWT (анонимный доступ)
  • checkSelects — массив проверок, выполняемых перед операцией
  • pathConditions — дополнительные условия фильтрации данных

Пример конфигурации для медицинской клиники

[
{
"name": "searchDoctorsForPatient",
"body": "query searchDoctorsForPatient($cond: String) { searchDoctor(cond: $cond) { elems { id person { entity { firstName lastName } } doctorType { name } } } }",
"allowEmptyChecks": false,
"disableJwtVerification": false,
"checkSelects": [
{
"conditionValue": "'patient' $in ${jwt:roles}",
"typeName": null
}
],
"pathConditions": []
},
{
"name": "searchDoctorAppointmentForDoctor",
"body": "query searchDoctorAppointmentForDoctor($cond: String!) { searchDoctorAppointment(cond: $cond) { elems { id beginDate endDate clinicCustomer { entity { customer { entity { person { entity { firstName lastName birthDate } } } } } } } } }",
"allowEmptyChecks": false,
"disableJwtVerification": false,
"checkSelects": [
{
"conditionValue": "'doctor' $in ${jwt:roles}",
"typeName": null
}
],
"pathConditions": [
{
"path": "searchDoctorAppointment",
"cond": "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId}"
}
]
},
{
"name": "createDoctorSchedule",
"body": "mutation createDoctorSchedule($input: _CreateDoctorScheduleInput!) { packet { lockClinicSchedule: getClinicSchedule(id:`${input.clinicSchedule}` lock:WAIT) {id} checkDoctorAndOfficeFreeSlot: getClinicSchedule(id:`find: it.id == ${input.clinicSchedule} && !it.doctorScheduleList{cond = (it.clinicDoctor.entityId == ${input.clinicDoctor.entityId} || it.clinicOffice.entityId == ${input.clinicOffice.entityId}) && it.beginDate <= ${input.endDate} && it.endDate >= ${input.beginDate}}.$exists` failOnEmpty:true) {id} createDoctorSchedule(input: $input) {id} } }",
"allowEmptyChecks": false,
"disableJwtVerification": false,
"checkSelects": [
{
"conditionValue": "'administrator' $in ${jwt:roles} || ${input.clinicDoctor} == ${jwt:clinicDoctorId}",
"typeName": null
}
],
"pathConditions": []
}
]

Типы проверок безопасности

1. CheckSelects — проверки, выполняемые перед операцией:

  • Проверяют права пользователя на выполнение операции
  • Могут использовать данные из JWT-токена
  • При неудачной проверке операция блокируется с ошибкой

2. PathConditions — фильтрация данных:

  • Накладывают дополнительные условия на выборку данных
  • Ограничивают видимые пользователю данные
  • Не вызывают ошибок, но могут возвращать пустой результат

Использование JWT-токенов в условиях

В строковых выражениях можно использовать подстановки из JWT:

{
"conditionValue": "'doctor' $in ${jwt:roles} && ${jwt:clinicId} == it.clinic.entityId"
}

Где:

  • ${jwt:roles} — массив ролей пользователя
  • ${jwt:clinicId} — ID клиники пользователя
  • ${jwt:clinicDoctorId} — ID врача (для роли doctor)

Настройка в DataSpace CE

  1. Включите безопасность в файле context-child.properties:
dataspace.security.graphql.permissions.source=file
dataspace.security.jwks.source=file
  1. Добавьте в файл graphql-permissions.json разрешения для всех операций, которые будут выполняться в приложении.

  2. Перезапустите DataSpace CE для применения настроек:

./quickstart.sh

Рекомендации по безопасности

  • Принцип минимальных привилегий: предоставляйте только необходимые права
  • Валидация входных данных: всегда проверяйте параметры запросов
  • Регулярное обновление: актуализируйте разрешения при изменении бизнес-логики

Правильно настроенный файл graphql-permissions.json обеспечивает надежную защиту данных медицинской клиники, разграничивая доступ между администраторами, врачами и пациентами согласно их ролям и полномочиям.

Проверки

Проверки (CheckSelects) — это механизм контроля доступа, который выполняется до выполнения GraphQL-операции. Если проверка не пройдена, операция блокируется с ошибкой авторизации. Это критически важный элемент безопасности, который предотвращает несанкционированный доступ к данным и операциям.

Принципы работы проверок

Последовательность выполнения:

  1. Пользователь отправляет GraphQL-запрос с JWT-токеном
  2. DataSpace CE проверяет подпись токена и извлекает данные пользователя
  3. Выполняются все условия из массива checkSelects
  4. Если все проверки пройдены успешно — операция выполняется
  5. Если хотя бы одна проверка провалена — возвращается ошибка 403 Forbidden

Типы проверок:

  • Проверка ролей — проверяет наличие определенной роли у пользователя
  • Проверка владельца — проверяет, что пользователь имеет право на данный ресурс
  • Проверка контекста — проверяет дополнительные условия (время, статус и т.д.)
  • Комбинированные проверки — сочетают несколько условий с логическими операторами

Практический пример: создание расписания врача

1. Фиксируем тело запроса в реестре допустимых

mutation createDoctorSchedule($input: _CreateDoctorScheduleInput!) {
packet {
lockClinicSchedule: getClinicSchedule(
id:`${input.clinicSchedule}`
lock:WAIT
) {id}


checkDoctorAndOfficeFreeSlot: getClinicSchedule(
id:`find:
it.id == ${input.clinicSchedule} &&
!it.doctorScheduleList{cond = (
it.clinicDoctor.entityId == ${input.clinicDoctor.entityId}
|| it.clinicOffice.entityId == ${input.clinicOffice.entityId}
) && it.beginDate <= ${input.endDate} && it.endDate >= ${input.beginDate}
}.$exists`
failOnEmpty:true
) {id}

createDoctorSchedule(input: $input) {id}
}
}

Разбор запроса:

  • lockClinicSchedule — блокирует расписание клиники для предотвращения конфликтов
  • checkDoctorAndOfficeFreeSlot — проверяет, что врач и кабинет свободны в указанное время
  • createDoctorSchedule — создает новую запись в расписании врача

2. Формируем условие ПРОВЕРКИ

'administrator' $in ${jwt:roles} || ${input.clinicDoctor} == ${jwt:clinicDoctorId}

Логика проверки:

  • Администратор ('administrator' $in ${jwt:roles}) может создавать расписание для любого врача
  • Врач (${input.clinicDoctor} == ${jwt:clinicDoctorId}) может создавать расписание только для себя
  • Пациент не имеет права выполнять эту операцию (проверка провалится)

Дополнительные примеры проверок

Проверка временных ограничений:

{
"conditionValue": "'administrator' $in ${jwt:roles} && ${environment.currentHour} >= 8 && ${environment.currentHour} <= 18",
"typeName": null
}

Проверка статуса пользователя:

{
"conditionValue": "'doctor' $in ${jwt:roles} && ${jwt:status} == 'ACTIVE' && ${jwt:license} == 'VALID'",
"typeName": null
}

Проверка принадлежности к клинике:

{
"conditionValue": "'patient' $in ${jwt:roles} && ${jwt:clinicId} == ${input.clinicId}",
"typeName": null
}

Типы условий в проверках

1. Проверка ролей:

"'administrator' $in ${jwt:roles}"          // Пользователь является администратором
"'doctor' $in ${jwt:roles}" // Пользователь является врачом
"'patient' $in ${jwt:roles}" // Пользователь является пациентом

2. Сравнение значений:

"${jwt:clinicDoctorId} == ${input.doctorId}"     // ID врача в токене совпадает с входным параметром
"${jwt:customerId} == ${input.customerId}" // ID клиента в токене совпадает с входным параметром

3. Логические операторы:

"'admin' $in ${jwt:roles} || 'doctor' $in ${jwt:roles}"              // ИЛИ
"'doctor' $in ${jwt:roles} && ${jwt:status} == 'ACTIVE'" // И
"!('patient' $in ${jwt:roles})" // НЕ

Фильтрация

Фильтрация данных (PathConditions) — это механизм безопасности, который автоматически ограничивает данные, возвращаемые пользователю, в зависимости от его прав доступа. В отличие от проверок (CheckSelects), фильтрация не блокирует операцию, а незаметно применяет дополнительные условия к выборке данных, обеспечивая принцип "нужно знать" (need-to-know).

Основные принципы фильтрации

Механизм работы:

  1. Пользователь выполняет GraphQL-запрос на получение данных
  2. DataSpace CE проверяет наличие PathConditions для данной операции
  3. Автоматически добавляет условия фильтрации к запросу
  4. Возвращает только те данные, которые пользователь имеет право видеть
  5. Пользователь не видит данных, к которым у него нет доступа

Ключевые отличия от проверок:

  • Проверки → блокируют операцию при неудаче (403 Forbidden)
  • Фильтрация → ограничивает данные, операция выполняется успешно

Преимущества подхода:

  • Прозрачность — пользователь не знает о существовании скрытых данных
  • Безопасность — предотвращает случайное раскрытие конфиденциальной информации
  • Гибкость — позволяет тонко настраивать видимость данных

Практический пример: просмотр записей врача

1. Фиксируем тело запроса в реестре допустимых

query searchDoctorAppointmentForDoctor($cond: String!){
searchDoctorAppointment(cond: $cond){
elems{
id
beginDate
endDate
clinicCustomer{
entity{
customer{
entity{
person{
entity{
firstName
lastName
birthDate
}}}}}
}
}
}
}

Разбор запроса:

  • searchDoctorAppointment — основной запрос для поиска записей к врачу
  • cond — параметр для дополнительной фильтрации (например, по дате)
  • Вложенная структура — получаем информацию о пациенте через связанные сущности
  • Безопасность — фильтрация гарантирует, что врач увидит только свои записи

2. Формируем условие ФИЛЬТРАЦИИ по полю searchDoctorAppointment

it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId}

Логика фильтрации:

  • it — каждая запись в результате поиска
  • it.doctorSchedule.clinicDoctor.entityId — ID врача из записи
  • ${jwt:clinicDoctorId} — ID врача из JWT-токена
  • Результат: врач видит только свои записи, даже если не указал это в параметре cond

Сценарии применения фильтрации

Сценарий 1: Многопользовательская система

Без фильтрации врач мог бы увидеть записи всех врачей:

# Опасный запрос без фильтрации
query getAllAppointments {
searchDoctorAppointment {
elems {
id
doctorSchedule { clinicDoctor { entity { person { entity { firstName lastName } } } } }
clinicCustomer { entity { customer { entity { person { entity { firstName lastName } } } } } }
}
}
}

С фильтрацией автоматически применяется условие:

{
"path": "searchDoctorAppointment",
"cond": "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId}"
}

Сценарий 2: Иерархическая фильтрация

Администратор видит всё, врач — только своё:

{
"path": "searchDoctorAppointment",
"cond": "'administrator' $in ${jwt:roles} || it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId}"
}

Сценарий 3: Временная фильтрация

Врач видит только актуальные записи:

{
"path": "searchDoctorAppointment",
"cond": "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId} && it.beginDate >= ${environment.currentDate}"
}

Сложные условия фильтрации

Фильтрация с множественными условиями:

{
"path": "searchPatientRecord",
"cond": "('doctor' $in ${jwt:roles} && it.attendingDoctor.entityId == ${jwt:clinicDoctorId}) || ('patient' $in ${jwt:roles} && it.patient.entityId == ${jwt:customerId}) || 'administrator' $in ${jwt:roles}"
}

Фильтрация с проверкой статуса:

{
"path": "searchDoctorSchedule",
"cond": "it.clinicDoctor.entityId == ${jwt:clinicDoctorId} && it.status == 'ACTIVE' && it.beginDate >= ${environment.today}"
}

Комбинирование проверок и фильтрации

Часто используется комбинированный подход:

{
"name": "getDoctorPatients",
"body": "query getDoctorPatients { searchDoctorAppointment { elems { clinicCustomer { entity { customer { entity { person { entity { firstName lastName } } } } } } } } }",
"checkSelects": [
{
"conditionValue": "'doctor' $in ${jwt:roles}",
"typeName": null
}
],
"pathConditions": [
{
"path": "searchDoctorAppointment",
"cond": "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId} && it.status == 'CONFIRMED'"
}
]
}

Логика работы:

  1. CheckSelects проверяет, что пользователь — врач
  2. PathConditions ограничивает данные только записями этого врача со статусом "CONFIRMED"

Безопасность фильтрации

Критически важные правила:

  1. Никогда не полагайтесь только на клиентскую фильтрацию — всегда применяйте PathConditions на сервере

  2. Тестируйте граничные случаи — убедитесь, что фильтрация работает для всех ролей и сценариев

  3. Используйте принцип минимальных привилегий — показывайте только необходимые данные

  4. Проверяйте цепочки связей — убедитесь, что через связанные сущности нельзя получить доступ к запрещенным данным

Фильтрация является критически важным компонентом безопасности медицинских систем, обеспечивая автоматическое разграничение доступа к конфиденциальным данным пациентов и соблюдение принципов врачебной тайны.

Рекомендации по интеграции механизмов безопасности в React-приложение

В данном подразделе будет предложен вариант интеграции механизма безапасности GraphQL (проверка и фильтрация) в React-приложение. Используя данные рекомендации и AI-ассистент GigaCode, вы сможете самостоятельно реализовать механизмы безопасности в своем приложении медицинской клиники.

Интеграция механизма проверок в React-приложение

Для успешной интеграции механизма проверок безопасности в React-приложение медицинской клиники необходимо выполнить несколько ключевых шагов, которые обеспечат корректную работу авторизации на всех уровнях системы.

1. Настройка конфигурации Keycloak в React-приложении

В корневой папке public размещается файл конфигурации keycloak.json, который содержит параметры подключения к серверу авторизации:

{
"realm": "clinic",
"auth-server-url": "http://localhost:8180/",
"ssl-required": "external",
"resource": "clinic",
"public-client": true,
"confidential-port": 0,
"useResourceRoleMappings": true
}

Параметр useResourceRoleMappings: true критически важен, так как указывает системе использовать client roles (роли клиента) вместо realm roles. Это позволяет получать роли administrator, doctor и patient, назначенные пользователям user1, user2 и user3 соответственно.

2. Создание GraphQL-запроса в React-приложении

В приложении создается GraphQL-мутация для создания расписания врача. Важно, чтобы тело запроса точно совпадало с тем, что указано в файле graphql-permissions.json:

// src/graphql/__generate/createDoctorSchedule.graphql
mutation createDoctorSchedule($input: _CreateDoctorScheduleInput!) {
packet {
lockClinicSchedule: getClinicSchedule(
id:`${input.clinicSchedule}`
lock:WAIT
) {id}

checkDoctorAndOfficeFreeSlot: getClinicSchedule(
id:`find:
it.id == ${input.clinicSchedule} &&
!it.doctorScheduleList{cond = (
it.clinicDoctor.entityId == ${input.clinicDoctor.entityId}
|| it.clinicOffice.entityId == ${input.clinicOffice.entityId}
) && it.beginDate <= ${input.endDate} && it.endDate >= ${input.beginDate}
}.$exists`
failOnEmpty:true
) {id}

createDoctorSchedule(input: $input) {id}
}
}

3. Генерация конфигурации разрешений

Для автоматического создания файла permissions.json используется специальная конфигурация. Команда npm run allgen запускает процесс генерации, который в том числе включает в себя создание файла permissions.json с базовой структурой разрешений:

  • Анализирует все GraphQL-файлы в папке src/graphql/__generate/
  • Создает JSON-описания для каждого запроса
  • Формирует в корне React-приложения файл permissions.json с базовой структурой разрешений

4. Настройка проверок безопасности в DataSpace CE

В файле graphql-permissions.json добавляется запись с условием проверки:

{
"name": "createDoctorSchedule",
"body": "mutation createDoctorSchedule($input: _CreateDoctorScheduleInput!) { ... }",
"checkSelects": [
{
"conditionValue": "'administrator' $in ${jwt:roles} || ${input.clinicDoctor} == ${jwt:clinicDoctorId}",
"typeName": null
}
],
"pathConditions": []
}

Это условие обеспечивает, что:

  • Администратор может создавать расписание для любого врача
  • Врач может создавать расписание только для себя (если его ID совпадает с ID в токене)
  • Пациент не может выполнять данную операцию

5. Интеграция JWT-токенов в Apollo Client

При создании Apollo Client в React-приложении JWT-токен от Keycloak автоматически добавляется в заголовки всех GraphQL-запросов:

const apolloClient = new ApolloClient({
uri: '/graphql',
headers: {
"Authorization": `Bearer ${keycloak.token}`
}
});

6. Принцип работы проверки

Когда пользователь выполняет операцию создания расписания:

  1. React-приложение отправляет GraphQL-запрос с JWT-токеном
  2. DataSpace CE получает запрос и проверяет подпись токена с помощью ключей из jwks.json
  3. Извлекает роли пользователя из токена (administrator, doctor или patient)
  4. Выполняет проверку условия: может ли данный пользователь создать расписание для указанного врача
  5. Если проверка пройдена — операция выполняется, если нет — возвращается ошибка 403 Forbidden

7. Обработка ошибок авторизации в React

В компонентах React рекомендуется предусмотреть обработку ошибок авторизации:

const [createSchedule] = useCreateDoctorScheduleMutation({
onError: (error) => {
if (error.message.includes('403') || error.message.includes('Forbidden')) {
message.error('У вас недостаточно прав для выполнения данной операции');
}
}
});

Заключение

Данный подход обеспечивает многоуровневую защиту: аутентификация происходит в Keycloak, авторизация проверяется в DataSpace CE, а пользовательский интерфейс в React может дополнительно скрывать недоступные функции на основе ролей пользователя. Это создает надежную систему безопасности для медицинского приложения, где критически важно разграничить доступ между администраторами, врачами и пациентами.

Интеграция механизма фильтрации в React-приложение

Интеграция механизма фильтрации обеспечивает автоматическое ограничение данных, которые видит пользователь, в соответствии с его ролью и правами доступа. В отличие от проверок, фильтрация не блокирует запрос, а незаметно ограничивает возвращаемые данные.

1. Настройка конфигурации Keycloak

Используется та же конфигурация keycloak.json, что и для проверок, с важным параметром useResourceRoleMappings: true для работы с client roles:

{
"realm": "clinic",
"auth-server-url": "http://localhost:8180/",
"ssl-required": "external",
"resource": "clinic",
"public-client": true,
"confidential-port": 0,
"useResourceRoleMappings": true
}

2. Создание GraphQL-запроса для поиска записей врача

В приложении создается GraphQL-запрос для получения записей к врачу. Тело запроса должно точно соответствовать записи в graphql-permissions.json:

// src/graphql/__generate/searchDoctorAppointmentForDoctor.graphql
query searchDoctorAppointmentForDoctor($cond: String!) {
searchDoctorAppointment(cond: $cond) {
elems {
id
beginDate
endDate
clinicCustomer {
entity {
customer {
entity {
person {
entity {
firstName
lastName
birthDate
}
}
}
}
}
}
}
}
}

3. Генерация базовой конфигурации разрешений

Команда npm run allgen автоматически создает запись для данного запроса в файле permissions.json с базовой структурой, которая затем переносится в graphql-permissions.json DataSpace CE с добавлением условий фильтрации.

4. Настройка фильтрации в DataSpace CE

В файле graphql-permissions.json добавляется запись с условием фильтрации в секции pathConditions:

{
"name": "searchDoctorAppointmentForDoctor",
"body": "query searchDoctorAppointmentForDoctor($cond: String!) { ... }",
"checkSelects": [
{
"conditionValue": "'doctor' $in ${jwt:roles}",
"typeName": null
}
],
"pathConditions": [
{
"path": "searchDoctorAppointment",
"cond": "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId}"
}
]
}

Логика фильтрации:

  • CheckSelects проверяет, что пользователь имеет роль doctor
  • PathConditions автоматически ограничивает результаты только записями к данному врачу
  • ${jwt:clinicDoctorId} — ID врача из JWT-токена, который сравнивается с ID врача в каждой записи

5. Принцип работы фильтрации

Когда врач выполняет поиск записей:

  1. React-приложение отправляет GraphQL-запрос с JWT-токеном и параметром поиска
  2. DataSpace CE проверяет роль пользователя (doctor)
  3. Автоматически добавляет условие фильтрации к запросу
  4. Возвращает только те записи, где doctorSchedule.clinicDoctor.entityId совпадает с ID врача из токена
  5. Врач видит только свои записи, даже если не указал это в параметре cond

6. Использование в React-компонентах

В React-компоненте врач может искать записи, не беспокоясь о дополнительной фильтрации — система автоматически покажет только его записи:

const DoctorAppointments: React.FC = () => {
const [searchAppointments, { data, loading }] = useSearchDoctorAppointmentForDoctorLazyQuery();

const handleSearch = (searchText: string) => {
// Врач может искать по любым критериям (дата, имя пациента и т.д.)
// Система автоматически ограничит результаты только его записями
searchAppointments({
variables: {
cond: searchText // Например: "it.beginDate >= '2024-01-01'"
}
});
};

return (
<div>
<Input.Search
placeholder="Поиск записей..."
onSearch={handleSearch}
/>
{data?.searchDoctorAppointment.elems.map(appointment => (
<div key={appointment.id}>
{appointment.clinicCustomer.entity.customer.entity.person.entity.firstName}
{/* Отображение только записей данного врача */}
</div>
))}
</div>
);
};

7. Комбинирование с дополнительными условиями

Врач может добавлять свои условия поиска, которые комбинируются с автоматической фильтрацией:

// Врач ищет записи на завтра
const searchTomorrowAppointments = () => {
searchAppointments({
variables: {
cond: "it.beginDate >= '2024-01-15' && it.beginDate < '2024-01-16'"
}
});
};

// Система автоматически добавит:
// "it.doctorSchedule.clinicDoctor.entityId == ${jwt:clinicDoctorId}"
// Итоговое условие будет:
// "it.doctorSchedule.clinicDoctor.entityId == 'doctor123' && it.beginDate >= '2024-01-15' && it.beginDate < '2024-01-16'"

Заключение

В данном разделе мы рассмотрели основные современные механизмы обеспечения безопасности на примере проверок и фильтрации данных в комплексном приложении медицинской клиники.

Вы узнали:

  • Как работает механизм авторизации и аутентификации в Keycloak
  • Как настроить Keycloak для работы с DataSpace CE
  • Как использовать механизмы проверки и фильтрации в GraphQL-запросах
  • Как интегрировать механизмы безопасности в React-приложение

Также было показано, как использовать AI-ассистента GigaCode для доработки собственного варианта механизмов безопасности в своем приложении на примере медицинской клиники.

Данный раздел завершает учебный курс по быстрой разработке приложений с DataSpace Community Edition. Для большей эффективности рекомендуем еще раз ознакомиться с ключевыми материалами курса и попробовать выполнить доработку приложения медицинской клиники с помощью AI-ассистента GigaCode.

Благодарим за внимание и желаем успехов в дальнейшем обучении и практической работе!

Ссылки